package com.blank.shop.ssr.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.blank.shop.ssr.dao.UserDao;
import com.blank.shop.ssr.entity.User;
import com.blank.shop.ssr.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 用户表(User)表服务实现类
 *
 * @author _blank
 * @since 2021-09-02 19:42:39
 */
@Slf4j
@CacheConfig(cacheNames = {"user"})
@Service("userService")
@Transactional(rollbackFor = {Exception.class})
public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserService {

//    private javax.cache.spi.CachingProvider jsr107CachingProvider;
//    private javax.cache.CacheManager jsr107CacheManager;
//    private javax.cache.Cache<String, Object> jsr107Cache;

//    private org.springframework.cache.CacheManager springCacheManager;
//    private org.springframework.cache.Cache springCache;

    // @org.springframework.cache.annotation.Caching 缓存组合模式
    // @org.springframework.cache.annotation.CacheConfig 缓存公共配置
    // @org.springframework.cache.annotation.Cacheable 缓存查询
    // @org.springframework.cache.annotation.CachePut 缓存更新
    // @org.springframework.cache.annotation.CacheEvict 缓存移除

    /**
     * 当前对象被容器托管，单例
     * https://www.cnblogs.com/codebj/p/10994748.html
     */
    private final Object lockObj = new Object();

    private final KeyGenerator keyGenerator = (target, method, params) -> "user-service-" + Arrays.toString(params);

    @Bean
    public KeyGenerator userKeyGenerator() {
        return this.keyGenerator;
    }

    @Resource
    private CacheManager cacheManager;

    @Resource
    private RedisTemplate<String, User> redisTemplate;

    @Cacheable(
//            key = "#root.methodName+'['+#id+']'",
            key = "'user-service-[' + #id + ']'",
//            keyGenerator = "userKeyGenerator",
//            condition = "#id > 0",
            condition = "#a0 > 1",
            unless = "#result == null"
    )
    @Override
    public User selectOne(Integer id) {
        return super.getById(id);
    }

    @CachePut(
//            key = "'user-service-' + '[' + #user.id + ']'"
            key = "'user-service-[' + #result.id + ']'"
    )
    @Override
    public User update(User user) {
        super.updateById(user);
        return user;
    }

    @Override
    public void addUser(User user) {
        log.info("User保存数据库前！");
        final boolean saveFlag = super.save(user);
        log.info("User保存数据库后 {}！", saveFlag);
        if (saveFlag) {
            // 到数据库里面，重新捞出新数据出来，做缓存
            final User dbUser = super.getById(user.getId());

            final Cache cache = cacheManager.getCache("user");
            if (Objects.nonNull(cache)) {
                // 往mysql里面插入成功随后再从mysql查询出来，再插入redis
                // org.springframework.data.redis.cache.CacheKeyPrefix
                cache.put("user::user-service-[" + dbUser.getId() + "]", dbUser);
            }
        }
    }

    @Override
    public void deleteUser(Integer id) {
        // 1.先直接删除数据库
        final boolean removeFlag = super.removeById(id);
        if (removeFlag) {
            final Cache cache = cacheManager.getCache("user");
            if (Objects.nonNull(cache)) {
                // 2.再修改缓存
                cache.evict("user::user-service-[" + id + "]");
            }
        }
    }

    @Override
    public void updateUser(User user) {
        //1.先直接修改数据库
        final boolean updateFlag = super.updateById(user);
        if (updateFlag) {
            //2.再修改缓存
            final User dbUser = super.getById(user.getId());

            final Cache cache = cacheManager.getCache("user");
            if (Objects.nonNull(cache)) {
                //修改也是用SET命令，重新设置，Redis没有update操作，都是重新设置新值
                cache.put("user::user-service-[" + dbUser.getId() + "]", dbUser);
            }
        }
    }

    /**
     * 先去redis里面找数据 ，找到就直接返回，找不到再去查询mysql
     *
     * @param userId userId
     * @return com.blank.shop.ssr.entity.User
     */
    @Override
    public User findUserById(Integer userId) {
        User user;

        final ValueOperations<String, User> valueOps = this.redisTemplate.opsForValue();
//        final Cache cache = cacheManager.getCache("user");
//        Assert.notNull(cache, "缓存user名称不能为空，请检查配置!");

        // 缓存key
        final String key = "user::user-service-[" + userId + "]";

        // 查询redis
        user = valueOps.get(key);

        // =======> 优化前
        // redis无，进一步查询mysql
//        if (Objects.isNull(user)) {
//            // 从mysql查出来user
//            user = super.getById(userId);
//            // mysql有，redis无
//            if (Objects.nonNull(user)) {
//                // 把mysql捞到的数据写入redis，方便下次查询能redis命中。
//                valueOps.set(key, user);
//            }
//            // else这块，mysql和redis都无数据
//        }

        //  =======> 优化后
        // 对于高QPS的请求，进来就先加锁，保证一个请求，让外面的redis等待一下，避免击穿mysql
        if (Objects.isNull(user)) {
            // 当前应用 分布式 的话，得用  Redisson  做分布式锁
            synchronized (this.lockObj) {
                // 再次查询redis还是null，可以去查mysql了（mysql默认有数据）
                user = valueOps.get(key);
                if (Objects.isNull(user)) {
                    // 从mysql查出来user
                    user = super.getById(userId);
                    if (Objects.nonNull(user)) {
                        // mysql里面有数据的，需要回写redis，完成数据一致性的同步工作
                        valueOps.setIfAbsent(key, user, 7L, TimeUnit.DAYS);
                    }
                }
            }
        }

        return user;
    }

}

